package edu.umn.cs5115.scheduler;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.tidy.Tidy;

import edu.umn.cs5115.scheduler.SearchQuery;
import edu.umn.cs5115.scheduler.entities.Course;
import edu.umn.cs5115.scheduler.entities.MeetingTime;
import edu.umn.cs5115.scheduler.entities.Section;
import edu.umn.cs5115.scheduler.entities.Weekday;

public class SearchThread extends Thread {
	private static final XPathFactory xPathFactory;
	private static final XPath xpath;
	
	private static SimpleDateFormat dateFormatter;
	private static SimpleDateFormat timeFormatter;
	static {
		xPathFactory = XPathFactory.newInstance();
		xpath = xPathFactory.newXPath();
		
		dateFormatter = new SimpleDateFormat("MM/dd/yyyy");
		timeFormatter = new SimpleDateFormat("hh:mm a");
	}
	
	private static Tidy tidy;
	
	private static XPathExpression rootTableRow, courseNumber, courseLabel, prereqs, lectureInfo, profInfo, roomInfo;
	private static Pattern courseNumberPattern, departmentPattern, sectionTypePattern, sectionDatePattern,sectionTimePattern;
	SchedulerDocument document;
	
	static {
		tidy = new Tidy(); 
		tidy.setQuiet(true);
		tidy.setShowWarnings(false);
		try {
			// get to the table.
			rootTableRow = xpath.compile("/html/body/form/table[@class='coursetable'][1]/tr[descendant::span[@class='label']]");
			// for the course name
			courseLabel = xpath.compile("descendant::span[@class='label'][1]/text()");
			// for prereqs and credit not granted, etc.
			prereqs = xpath.compile("descendant::span[@class='bodytext'][2]/text()");
			// for lecture information which should always follow.
			
			// section info based on nextSection context.
			lectureInfo = xpath.compile("child::td[@class='bodytext']/text()");
			// for the professors.  Based on lectureInfo context.
			profInfo = xpath.compile("following-sibling::a[contains(@href, 'lookup')]/text()");
			// for room - Assume only a single room is listed.  Based on lectureInfo context.
			roomInfo = xpath.compile("following-sibling::a[contains(@href, '#')]/text()");
			// Course Number
			courseNumber = xpath.compile("td[2]/span[@class='sectiontitle']/text()");
			
			courseNumberPattern = Pattern.compile("\\d{4,4}[Ww]?", Pattern.CANON_EQ);
			departmentPattern = Pattern.compile("[A-Z]{2,4}");
			sectionTypePattern = Pattern.compile("\\d{3}\\s*(\\w{3})");
			sectionDatePattern = Pattern.compile("\\((\\d\\d/\\d\\d/\\d{4}) - (\\d\\d/\\d\\d/\\d{4})\\)");
			sectionTimePattern = Pattern.compile("(\\d\\d:\\d\\d [AP]\\.M.) - (\\d\\d:\\d\\d [AP]\\.M.)");
			// broken regex for everyting: Pattern.compile("\\d{3}\\s*(\\w{3})\\s*.*(?>(\\d{2}:\\d{2})\\s(P\\.M\\.|A\\.M\\.)\\s-\\s(\\d{2}:\\d{2})\\s(P\\.M\\.|A\\.M\\.)\\s*.*?(([A-Z]{1,2}[,|\\s]){1,5}))?");
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
	
	SearchQuery query;
	
	public void run() {
		// load up the defaults.
		
		// Some variables used in various places to support parsing mostly.
		String getRequestString, courseLabelValue, startColor,lectureDetails,sectionType,temp;
	    Node startNode,resultNode;
	    NodeList resultNodes, lectureResults;
	    Course currentCourse;
	    Section currentSection, parentSection = null;
	    MeetingTime currentMeetingTime;
	    Matcher matcher;
	    Integer current5DigitNumber;
	    List<Course> results = new ArrayList<Course>();
		boolean nextIsChild= false, isChild = false; // used to derive relationships among lectures and otherwise.
		
		try {
			// construct the string to pass to the website.
			getRequestString = constructGetString(query);
			// get the URL with the new string.
			
			URL searchURL = new URL("http://onestop2.umn.edu/courseinfo/viewSearchResults.do?".concat(getRequestString));
		
			// Use Tidy to return an XML DOM that we can walk through.
			Document doc = tidy.parseDOM(new InterruptableInputStream(searchURL.openStream()), null);
			
			// parse out the root table row.
			Object result = rootTableRow.evaluate(doc, XPathConstants.NODESET);
		    NodeList rootTableRowNodes = (NodeList) result;
		    // each iteration is a distinct course if XPath serves me right.
		    for(int i = 0; i < rootTableRowNodes.getLength(); i++)  {
		    	// pull out the course name.
			    result = courseLabel.evaluate(rootTableRowNodes.item(i), XPathConstants.NODE);
			    Node courseLabelNode = (Node) result;
			    if(courseLabelNode != null) {
			    	// means we've found a class
			    	// default is no subsections.
			    	nextIsChild = false;
			    	
			    	// set up our new course.
			    	currentCourse = new Course(document);
			    	
			    	// pull out the course label (eg. "CSCI 3081W Program Design and Development")
			    	courseLabelValue = courseLabelNode.getNodeValue();
			    	
			    	// parse out department name
			    	matcher = departmentPattern.matcher(courseLabelValue);
			    	matcher.find();
			    	currentCourse.setDepartment(matcher.group());
			    	
			    	// parse out course number
			    	matcher = courseNumberPattern.matcher(courseLabelValue);
			    	matcher.find();
			    	currentCourse.setCourseNumber(matcher.group());
			    	currentCourse.setDescription("Student may contact instructor for additional information.");
			    	// parse out course description
			    	currentCourse.setName(courseLabelValue.substring(matcher.end()+1));
				    
					// get prereqs and such.
					result = prereqs.evaluate(rootTableRowNodes.item(i), XPathConstants.NODE);
				    resultNode = (Node) result;
				    currentCourse.setPrereqs(resultNode.getNodeValue());
				    
				    /* Debugging Printing.
			    	System.out.println(currentCourse.getDepartment());
				    System.out.println(currentCourse.getCourseNumber());
				    System.out.println("");
				    System.out.println(currentCourse.getName());
				    System.out.println(currentCourse.getPrereqs());
				    */
				    
				    // These values are used to properly accumulate all the sections and sub-sections.
			    	startNode = rootTableRowNodes.item(i).getNextSibling().getNextSibling();
			    	startColor = startNode.getAttributes().getNamedItem("class").getNodeValue();
			    	
					while(startNode != null &&
							startNode.getAttributes().getNamedItem("class") != null &&
							startNode.getAttributes().getNamedItem("class").getNodeValue().equals(startColor)
						  ) {
						// pull out a lecture section
						result = lectureInfo.evaluate(startNode, XPathConstants.NODESET);
					    lectureResults = (NodeList) result;
						if(lectureResults != null && lectureResults.getLength() > 0) {
							// LectureDetails is the line that contains times, dates, lecture type, location, and credits.
							lectureDetails = "";
							for(int j = 0; j < lectureResults.getLength(); j++)  {
								lectureDetails = lectureDetails.concat(lectureResults.item(j).getNodeValue());
							}
							// Pull out the section type.  Must do this first as we have to see if we need to skip this section.
							try {
								matcher = sectionTypePattern.matcher(lectureDetails);
								matcher.find();
								sectionType = matcher.group(1);
							}
							catch(IllegalStateException e) {
								// this will occur when a class is cancelled or otherwise unparsable because 
								// someone dumped a ton of crap into that field that is unrecognizable by a 
								// regular language.
								startNode = startNode.getNextSibling().getNextSibling();
								continue;
							}
							
							if(sectionType.equals("LEC") && nextIsChild) {
								// we've been inserting children, and we come across a new lecture.  Time to reset.
								// POSSIBLE BUG HERE: If there are other parent sections that are something other than LEC
								// (DIS, LET, etc.), this will not work.  I have yet to see something like this, however.
								nextIsChild = false;
								isChild = false;
							}
							
							// Grab 5 digit course number.
							result = courseNumber.evaluate(startNode, XPathConstants.NODE);
							resultNode = (Node) result;
							temp = resultNode.getNodeValue().replace("\u00A0", "").trim();
							if(result != null && temp.length() > 0) {
								isChild = nextIsChild;
								current5DigitNumber = Integer.parseInt(temp);
							}
							else {
								current5DigitNumber = 0;
								// 5 digit number is absent.  Must have a child.
								if(nextIsChild) {
									isChild = true;
									// no number, but this one is already child.
									// therefore the next one is ALSO a child.  Don't reset nextIsChild value, add this as child and continue on.
								}
								else {
									isChild = false;
									// No number, this one must be parent.
									nextIsChild = true;
								}
									
							}
							
							// Now that we've got the above straight, lets instantiate our section so we can start filling it with data.
							if(isChild) {
								currentSection = new Section(parentSection);
							}
							else {
								currentSection = new Section(currentCourse);
								parentSection = currentSection;
							}
							
							// set the section type, since we have this already.
							currentSection.setType(sectionType);
							
							// And the 5 digit number, since we have that already too.
							if(current5DigitNumber > 0)
								currentSection.setSectionNumber(current5DigitNumber.toString());
							else
								currentSection.setSectionNumber("");

							
							// Collects the profs.  Some courses have multiple profs.
							result = profInfo.evaluate(lectureResults.item(0), XPathConstants.NODESET);
							resultNodes = (NodeList) result;
							for(int j = 0; j < resultNodes.getLength(); j++)  {
								// if there's already a prof, add it, otherwise don't.
								if(currentSection.getStaff() == null)
									currentSection.setStaff(resultNodes.item(j).getNodeValue());
								else
									currentSection.setStaff(currentSection.getStaff().concat(" ").concat(resultNodes.item(j).getNodeValue()));
							}
							
							// Grab our meeting times.
							currentMeetingTime = new MeetingTime(document, currentSection);
							
							// meeting location
							result = roomInfo.evaluate(lectureResults.item(0), XPathConstants.NODE);
							resultNode = (Node) result;
							if(result != null)
								currentMeetingTime.setLocation(resultNode.getNodeValue());
							else
								currentMeetingTime.setLocation("Not Found");
							
							// date the class runs.
							matcher = sectionDatePattern.matcher(lectureDetails);
							matcher.find();
							if(matcher.groupCount() >= 2) {
								currentMeetingTime.setStartDate(dateFormatter.parse(matcher.group(1)));
								currentMeetingTime.setEndDate(dateFormatter.parse(matcher.group(2)));
							}
							
							// time of the class.
							matcher = sectionTimePattern.matcher(lectureDetails);
							matcher.find();
							if(matcher.groupCount() >= 2) {
								currentMeetingTime.setStartTime(timeFormatter.parse(matcher.group(1).replace(".", "")));
								currentMeetingTime.setEndTime(timeFormatter.parse(matcher.group(2).replace(".", "")));
								
								// this pulls out the days.  It MAY depend on having the time present, so to be safe,
								// if classes with no times are ever handled, check this being absent also..
								for(String day : lectureDetails.substring(matcher.end()+3,lectureDetails.indexOf("(", matcher.end()+4)-1).split(",")) {
									if(day.equals("M"))
										currentMeetingTime.addMeetingDay(Weekday.MONDAY);
									else if(day.equals("Tu"))
										currentMeetingTime.addMeetingDay(Weekday.TUESDAY);
									else if(day.equals("W"))
										currentMeetingTime.addMeetingDay(Weekday.WEDNESDAY);
									else if(day.equals("Th"))
										currentMeetingTime.addMeetingDay(Weekday.THURSDAY);
									else if(day.equals("F"))
										currentMeetingTime.addMeetingDay(Weekday.FRIDAY);
									else if(day.equals("Sa"))
										currentMeetingTime.addMeetingDay(Weekday.SATURDAY);
									
								}
							}
							else {
								// no times... continue at this point.
								// TODO: Enter handling of classes with no time here.
								continue;
							}

							
							// cludge for now.
							currentSection.setCredits(0);
							currentSection.addMeetingTime(currentMeetingTime);
					    }
						
						startNode = startNode.getNextSibling().getNextSibling();
					}
					
					results.add(currentCourse);
			    }
		    }
		}
		catch (Exception e) {
			e.printStackTrace();
		}
                if (!isInterrupted())
                    query.sendSearchCompleted(results);
		return;
	}
	
	public void setSearchQuery(SearchQuery inQuery) {
		query = inQuery;
	}
	
	public void setDocument(SchedulerDocument doc) {
		document = doc;
	}
	/*
	 * This function does the work of converting the search query object
	 * into a string that can be passed to the web site for searching.
	 * 
	 * The first step happens to be converting the human likable form entries
	 * to the codes the website wants.
	 */
	private String constructGetString(SearchQuery query) throws UnsupportedEncodingException {
		Hashtable <String, Object> currentParams = query.getParams();
		
		StringBuffer getRequest = new StringBuffer("institution=UMNTC&search=Search");
		
		// first go through the ones with arrays and take care of setting the codes right.
		if(currentParams.containsKey("searchCourseAttributes")) {
			for(String value : (ArrayList<String>)currentParams.get("searchCourseAttributes")) {
				getRequest.append("&searchCourseAttributes=");
				if(WebInterface.libEdCodes.containsKey(value.toLowerCase()))
					getRequest.append(URLEncoder.encode(WebInterface.libEdCodes.get(value.toLowerCase()), "ISO-8859-1"));
				else
					getRequest.append(URLEncoder.encode(value, "ISO-8859-1"));
			}
			currentParams.remove("searchCourseAttributes");
		}
		
		if(currentParams.containsKey("searchSessionCodes")) {
			for(String value : (ArrayList<String>)currentParams.get("searchSessionCodes")) {
				getRequest.append("&searchSessionCodes=");
				if(WebInterface.sessionCodes.containsKey(value.toLowerCase()))
					getRequest.append(URLEncoder.encode(WebInterface.sessionCodes.get(value.toLowerCase()), "ISO-8859-1"));
				else
					getRequest.append(URLEncoder.encode(value, "ISO-8859-1"));
			}
			currentParams.remove("searchSessionCodes");
		}
		
		if(currentParams.containsKey("searchLocations")) {
			for(String value : (ArrayList<String>)currentParams.get("searchLocations")) {
				getRequest.append("&searchLocations=");
				if(WebInterface.campusAreaCodes.containsKey(value.toLowerCase()))
					getRequest.append(URLEncoder.encode(WebInterface.campusAreaCodes.get(value.toLowerCase()), "ISO-8859-1"));
				else
					getRequest.append(URLEncoder.encode(value, "ISO-8859-1"));
				
				getRequest.append("%2Cx");
			}
			currentParams.remove("searchLocations");
		}
		
		// term needs the proper code applied.  Check to see if its specified first, but
		// it should always be specified.  Can't hurt to check though, I guess.
		
		if(currentParams.containsKey("searchTerm")) {
			getRequest.append("&searchTerm=");
			if(WebInterface.getWebInterface().getAcademicSessionCodeHash().containsKey(((String)currentParams.get("searchTerm")).toLowerCase()))
				getRequest.append(URLEncoder.encode(WebInterface.getWebInterface().getAcademicSessionCodeHash().get(((String)currentParams.get("searchTerm")).toLowerCase()), "ISO-8859-1"));	
			else
				getRequest.append(((String)currentParams.get("searchTerm")));
			currentParams.remove("searchTerm");
		}
		
		
		// Search Subjects...
		
		if(currentParams.containsKey("searchSubjects")) {
			for(String value : (ArrayList<String>)currentParams.get("searchSubjects")) {
				getRequest.append("&searchSubjects=");
				getRequest.append(URLEncoder.encode(value, "ISO-8859-1"));
				// easter egg?  ;)
				getRequest.append(URLEncoder.encode("|requested from class scheduler - thanks onestop!", "ISO-8859-1"));
			}
			currentParams.remove("searchSubjects");
		}
		
		// Level Limits
		if(currentParams.containsKey("searchLowerLevelLimit")) {
			getRequest.append("&searchLowerLevelLimit=");
			getRequest.append(URLEncoder.encode((String)currentParams.get("searchLowerLevelLimit"), "ISO-8859-1"));
			getRequest.append("%2Cx");
			currentParams.remove("searchLowerLevelLimit");
			
		}
		
		if(currentParams.containsKey("searchUpperLevelLimit")) {
			getRequest.append("&searchUpperLevelLimit=");
			getRequest.append(URLEncoder.encode((String)currentParams.get("searchUpperLevelLimit"), "ISO-8859-1"));
			getRequest.append("%2Cx");
			currentParams.remove("searchUpperLevelLimit");
		}
		
		// Time Limits
		if(currentParams.containsKey("searchLowerStartTime")) {
			getRequest.append("&searchLowerStartTime=");
			
			// This will cut off the 0 at the beginning of the time.  Basically what this does is remove
			// any classes that do not have specified times from the search results while still preserving
			// the actual time searched for.
			
			// TODO: REMOVE ALL BUT THE ELSE STATEMENT IF WE EVER START HANDLING CLASSES W/ NO TIME.
			if(((String)currentParams.get("searchLowerStartTime")).startsWith("0"))
				getRequest.append(URLEncoder.encode(((String)currentParams.get("searchLowerStartTime")).substring(1), "ISO-8859-1"));
			else
				getRequest.append(URLEncoder.encode((String)currentParams.get("searchLowerStartTime"), "ISO-8859-1"));
			getRequest.append("%2Cx");
			currentParams.remove("searchLowerStartTime");
			
		}
		
		if(currentParams.containsKey("searchUpperEndTime")) {
			getRequest.append("&searchUpperEndTime=");
			getRequest.append(URLEncoder.encode((String)currentParams.get("searchUpperEndTime"), "ISO-8859-1"));
			getRequest.append("%2Cx");
			currentParams.remove("searchUpperEndTime");
		}
		// Now all the rest can be done in a more general way.
		String currentKey, currentValue;
		for(Enumeration e = currentParams.keys() ; e.hasMoreElements() ;) {
			currentKey = (String)e.nextElement();
			currentValue = (String) currentParams.get(currentKey);
			
			getRequest.append("&");
			getRequest.append(currentKey);
			getRequest.append("=");
			getRequest.append(URLEncoder.encode(currentValue, "ISO-8859-1"));
		}
		
		// now we have a query string ready to be appended.
		return getRequest.toString();
	}
}
